// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "AppCommandlineArgs.h"
#include "../types/inc/utils.hpp"
#include "TerminalSettingsModel/ModelSerializationHelpers.h"
#include <LibraryResources.h>

using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace TerminalApp;

// Either a ; at the start of a line, or a ; preceded by any non-\ char.
const std::wregex AppCommandlineArgs::_commandDelimiterRegex{ LR"(^;|[^\\];)" };

AppCommandlineArgs::AppCommandlineArgs()
{
    _buildParser();
    _resetStateToDefault();
}

// Method Description:
// - Attempt to parse a given command as a single commandline. If the command
//   doesn't have a subcommand, we'll try parsing the commandline again, as a
//   new-tab command.
// - Actions generated by this command are added to our _startupActions list.
// Arguments:
// - command: The individual commandline to parse as a command.
// Return Value:
// - 0 if the commandline was successfully parsed
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs::ParseCommand(const Commandline& command)
{
    const auto argc = static_cast<int>(command.Argc());

    // Stash a pointer to the current Commandline instance we're parsing.
    // When we're trying to parse the commandline for a new-tab/split-pane
    // subcommand, we'll need to inspect the original Args from this
    // Commandline to find the entirety of the commandline args for the new
    // terminal instance. Discard the pointer when we leave this method. The
    // pointer will be safe for usage, since the parse callback will be
    // executed on the same thread, higher on the stack.
    _currentCommandline = &command;
    auto clearPointer = wil::scope_exit([this]() { _currentCommandline = nullptr; });
    try
    {
        // CLI11 needs a mutable vector<string>, so copy out the args here.
        // * When we're using the vector<string> parse(), it also expects that
        //   there isn't a leading executable name in the args, so slice that
        //   out.
        //   - In AppCommandlineArgs::BuildCommands, we'll make sure each
        //     subsequent command in a single commandline starts with a wt.exe.
        //     Our very first argument might not be "wt.exe", it could be `wt`,
        //     or `wtd.exe`, etc. Regardless, we want to ignore the first arg of
        //     every Commandline
        // * Not only that, but this particular overload of parse() wants the
        //   args _reversed_ here.
        std::vector<std::string> args{ command.Args().begin() + 1, command.Args().end() };
        std::reverse(args.begin(), args.end());

        // Revert our state to the initial state. As this function can be called
        // multiple times during the parsing of a single commandline (once for each
        // sub-command), we don't want the leftover state from previous calls to
        // pollute this run's state.
        _resetStateToDefault();

        // Manually check for the "/?" or "-?" flags, to manually trigger the help text.
        if (argc == 2 && (NixHelpFlag == til::at(command.Args(), 1) || WindowsHelpFlag == til::at(command.Args(), 1)))
        {
            throw CLI::CallForHelp();
        }

        // attempt to parse the commandline prefix of the form [options][subcommand]
        _app.parse(args);
        auto remainingParams = _app.remaining_size();

        // If we parsed the commandline, and _no_ subcommands were provided, try
        // parse the remaining suffix as a "new-tab" command.
        if (_noCommandsProvided())
        {
            _newTabCommand.subcommand->parse(args);
            remainingParams = _newTabCommand.subcommand->remaining_size();
        }

        // if after parsing the prefix and (optionally) the implicit tab subcommand
        // we still have unparsed parameters we need to fail
        if (remainingParams > 0)
        {
            throw CLI::ExtrasError(args);
        }
    }
    catch (const CLI::ParseError& e)
    {
        return _handleExit(_app, e);
    }
    return 0;
}

// Method Description:
// - Calls App::exit() for the provided command, and collects its output into
//   our _exitMessage buffer.
// Arguments:
// - command: Either the root App object, or a subcommand for which to call exit() on.
// - e: the CLI::Error to process as the exit reason for parsing.
// Return Value:
// - 0 if the command exited successfully
// - nonzero return values are defined in CLI::ExitCodes
int AppCommandlineArgs::_handleExit(const CLI::App& command, const CLI::Error& e)
{
    // Create some streams to collect the output that would otherwise go to stdout.
    std::ostringstream out;
    std::ostringstream err;
    const auto result = command.exit(e, out, err);
    // I believe only CallForHelp will return 0
    if (result == 0)
    {
        _exitMessage = out.str();
    }
    else
    {
        _exitMessage = err.str();
    }

    // We're displaying an error message - we should always exit instead of
    // actually starting the Terminal.
    _shouldExitEarly = true;

    return result;
}

// Method Description:
// - Add each subcommand and options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildParser()
{
    // We define or parser as a prefix command, to support "implicit new tab subcommand" scenario.
    // In this scenario we will try to parse the prefix that contains parameters like launch mode,
    // but will not encounter an explicit command.
    // Instead we will encounter an argument that doesn't belong to the prefix indicating the prefix is over.
    // Then we will try to parse the remaining arguments as a new tab subcommand.
    // E.g., for "wt.exe -M -d c:/", we will use -M for the launch mode, but once we will encounter -d
    // we will know that the prefix is over and try to handle the suffix as a new tab subcommand
    _app.prefix_command();

    // -v,--version: Displays version info
    auto versionCallback = [this](int64_t /*count*/) {
        // Set our message to display the application name and the current version.
        _exitMessage = fmt::format("{0}\n{1}",
                                   til::u16u8(CascadiaSettings::ApplicationDisplayName()),
                                   til::u16u8(CascadiaSettings::ApplicationVersion()));
        // Theoretically, we don't need to exit now, since this isn't really
        // an error case. However, in practice, it feels weird to have `wt
        // -v` open a new tab, and makes enough sense that `wt -v ;
        // split-pane` (or whatever) just displays the version and exits.
        _shouldExitEarly = true;
    };
    _app.add_flag_function("-v,--version", versionCallback, RS_A(L"CmdVersionDesc"));

    // Launch mode related flags
    //   -M,--maximized: Maximizes the window on launch
    //   -F,--fullscreen: Fullscreens the window on launch
    //   -f,--focus: Sets the terminal into the Focus mode
    // While fullscreen excludes both maximized and focus mode, the user can combine between the maximized and focused (-fM)
    auto maximizedCallback = [this](int64_t /*count*/) {
        _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::FocusMode) ?
                          LaunchMode::MaximizedFocusMode :
                          LaunchMode::MaximizedMode;
    };
    auto fullscreenCallback = [this](int64_t /*count*/) {
        _launchMode = LaunchMode::FullscreenMode;
    };
    auto focusCallback = [this](int64_t /*count*/) {
        _launchMode = (_launchMode.has_value() && _launchMode.value() == LaunchMode::MaximizedMode) ?
                          LaunchMode::MaximizedFocusMode :
                          LaunchMode::FocusMode;
    };

    auto maximized = _app.add_flag_function("-M,--maximized", maximizedCallback, RS_A(L"CmdMaximizedDesc"));
    auto fullscreen = _app.add_flag_function("-F,--fullscreen", fullscreenCallback, RS_A(L"CmdFullscreenDesc"));
    auto focus = _app.add_flag_function("-f,--focus", focusCallback, RS_A(L"CmdFocusDesc"));
    maximized->excludes(fullscreen);
    focus->excludes(fullscreen);

    auto positionCallback = [this](std::string string) {
        _position = LaunchPositionFromString(string);
    };
    _app.add_option_function<std::string>("--pos", positionCallback, RS_A(L"CmdPositionDesc"));
    auto sizeCallback = [this](std::string string) {
        _size = SizeFromString(string);
    };
    _app.add_option_function<std::string>("--size", sizeCallback, RS_A(L"CmdSizeDesc"));

    _app.add_option("-w,--window",
                    _windowTarget,
                    RS_A(L"CmdWindowTargetArgDesc"));

    _app.add_option("-s,--saved",
                    _loadPersistedLayoutIdx,
                    RS_A(L"CmdSavedLayoutArgDesc"));

    // Subcommands
    _buildNewTabParser();
    _buildSplitPaneParser();
    _buildFocusTabParser();
    _buildMoveFocusParser();
    _buildMovePaneParser();
    _buildSwapPaneParser();
    _buildFocusPaneParser();
    _buildSaveSnippetParser();
}

// Method Description:
// - Adds the `new-tab` subcommand and related options to the commandline parser.
// - Additionally adds the `nt` subcommand, which is just a shortened version of `new-tab`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildNewTabParser()
{
    _newTabCommand.subcommand = _app.add_subcommand("new-tab", RS_A(L"CmdNewTabDesc"));
    _newTabShort.subcommand = _app.add_subcommand("nt", RS_A(L"CmdNTDesc"));

    auto setupSubcommand = [this](auto& subcommand) {
        _addNewTerminalArgs(subcommand);

        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand.subcommand->callback([&, this]() {
            // Build the NewTab action from the values we've parsed on the commandline.
            ActionAndArgs newTabAction{};
            newTabAction.Action(ShortcutAction::NewTab);
            // _getNewTerminalArgs MUST be called before parsing any other options,
            // as it might clear those options while finding the commandline
            NewTabArgs args{ _getNewTerminalArgs(subcommand) };
            newTabAction.Args(args);
            _startupActions.push_back(newTabAction);
        });
    };

    setupSubcommand(_newTabCommand);
    setupSubcommand(_newTabShort);
}

// Method Description:
// - Adds the `split-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `sp` subcommand, which is just a shortened version of `split-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildSplitPaneParser()
{
    _newPaneCommand.subcommand = _app.add_subcommand("split-pane", RS_A(L"CmdSplitPaneDesc"));
    _newPaneShort.subcommand = _app.add_subcommand("sp", RS_A(L"CmdSPDesc"));

    auto setupSubcommand = [this](auto& subcommand) {
        _addNewTerminalArgs(subcommand);
        subcommand._horizontalOption = subcommand.subcommand->add_flag("-H,--horizontal",
                                                                       _splitHorizontal,
                                                                       RS_A(L"CmdSplitPaneHorizontalArgDesc"));
        subcommand._verticalOption = subcommand.subcommand->add_flag("-V,--vertical",
                                                                     _splitVertical,
                                                                     RS_A(L"CmdSplitPaneVerticalArgDesc"));
        subcommand._verticalOption->excludes(subcommand._horizontalOption);
        auto* sizeOpt = subcommand.subcommand->add_option("-s,--size",
                                                          _splitPaneSize,
                                                          RS_A(L"CmdSplitPaneSizeArgDesc"));

        subcommand._duplicateOption = subcommand.subcommand->add_flag("-D,--duplicate",
                                                                      _splitDuplicate,
                                                                      RS_A(L"CmdSplitPaneDuplicateArgDesc"));
        sizeOpt->check(CLI::Range(0.01f, 0.99f));

        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand.subcommand->callback([&, this]() {
            // Build the SplitPane action from the values we've parsed on the commandline.
            ActionAndArgs splitPaneActionAndArgs{};
            splitPaneActionAndArgs.Action(ShortcutAction::SplitPane);

            // _getNewTerminalArgs MUST be called before parsing any other options,
            // as it might clear those options while finding the commandline
            auto terminalArgs{ _getNewTerminalArgs(subcommand) };
            auto style{ SplitDirection::Automatic };
            // Make sure to use the `Option`s here to check if they were set -
            // _getNewTerminalArgs might reset them while parsing a commandline
            if ((*subcommand._horizontalOption || *subcommand._verticalOption))
            {
                if (_splitHorizontal)
                {
                    style = SplitDirection::Down;
                }
                else if (_splitVertical)
                {
                    style = SplitDirection::Right;
                }
            }
            const auto splitMode{ subcommand._duplicateOption && _splitDuplicate ? SplitType::Duplicate : SplitType::Manual };
            SplitPaneArgs args{ splitMode, style, _splitPaneSize, terminalArgs };
            splitPaneActionAndArgs.Args(args);
            _startupActions.push_back(splitPaneActionAndArgs);
        });
    };

    setupSubcommand(_newPaneCommand);
    setupSubcommand(_newPaneShort);
}
// Method Description:
// - Adds the `move-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `mp` subcommand, which is just a shortened version of `move-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMovePaneParser()
{
    _movePaneCommand = _app.add_subcommand("move-pane", RS_A(L"CmdMovePaneDesc"));
    _movePaneShort = _app.add_subcommand("mp", RS_A(L"CmdMPDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        subcommand->add_option("-t,--tab",
                               _movePaneTabIndex,
                               RS_A(L"CmdMovePaneTabArgDesc"));

        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            // Build the action from the values we've parsed on the commandline.
            ActionAndArgs movePaneAction{};

            if (_movePaneTabIndex >= 0)
            {
                movePaneAction.Action(ShortcutAction::MovePane);
                MovePaneArgs args{ static_cast<unsigned int>(_movePaneTabIndex), L"" };
                movePaneAction.Args(args);
                _startupActions.push_back(movePaneAction);
            }
        });
    };
    setupSubcommand(_movePaneCommand);
    setupSubcommand(_movePaneShort);
}

// Method Description:
// - Adds the `focus-tab` subcommand and related options to the commandline parser.
// - Additionally adds the `ft` subcommand, which is just a shortened version of `focus-tab`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildFocusTabParser()
{
    _focusTabCommand = _app.add_subcommand("focus-tab", RS_A(L"CmdFocusTabDesc"));
    _focusTabShort = _app.add_subcommand("ft", RS_A(L"CmdFTDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        auto* indexOpt = subcommand->add_option("-t,--target",
                                                _focusTabIndex,
                                                RS_A(L"CmdFocusTabTargetArgDesc"));
        auto* nextOpt = subcommand->add_flag("-n,--next",
                                             _focusNextTab,
                                             RS_A(L"CmdFocusTabNextArgDesc"));
        auto* prevOpt = subcommand->add_flag("-p,--previous",
                                             _focusPrevTab,
                                             RS_A(L"CmdFocusTabPrevArgDesc"));
        nextOpt->excludes(prevOpt);
        indexOpt->excludes(prevOpt);
        indexOpt->excludes(nextOpt);

        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            // Build the action from the values we've parsed on the commandline.
            ActionAndArgs focusTabAction{};

            if (_focusTabIndex >= 0)
            {
                focusTabAction.Action(ShortcutAction::SwitchToTab);
                SwitchToTabArgs args{ static_cast<unsigned int>(_focusTabIndex) };
                focusTabAction.Args(args);
                _startupActions.push_back(focusTabAction);
            }
            else if (_focusNextTab || _focusPrevTab)
            {
                focusTabAction.Action(_focusNextTab ? ShortcutAction::NextTab : ShortcutAction::PrevTab);
                // GH#10070 - make sure to not use the MRU order when switching
                // tabs on the commandline. That wouldn't make any sense!
                focusTabAction.Args(_focusNextTab ?
                                        static_cast<IActionArgs>(NextTabArgs(TabSwitcherMode::Disabled)) :
                                        static_cast<IActionArgs>(PrevTabArgs(TabSwitcherMode::Disabled)));
                _startupActions.push_back(std::move(focusTabAction));
            }
        });
    };

    setupSubcommand(_focusTabCommand);
    setupSubcommand(_focusTabShort);
}

static const std::map<std::string, FocusDirection> focusDirectionMap = {
    { "left", FocusDirection::Left },
    { "right", FocusDirection::Right },
    { "up", FocusDirection::Up },
    { "down", FocusDirection::Down },
    { "previous", FocusDirection::Previous },
    { "nextInOrder", FocusDirection::NextInOrder },
    { "previousInOrder", FocusDirection::PreviousInOrder },
    { "first", FocusDirection::First },
};

// Method Description:
// - Adds the `move-focus` subcommand and related options to the commandline parser.
// - Additionally adds the `mf` subcommand, which is just a shortened version of `move-focus`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildMoveFocusParser()
{
    _moveFocusCommand = _app.add_subcommand("move-focus", RS_A(L"CmdMoveFocusDesc"));
    _moveFocusShort = _app.add_subcommand("mf", RS_A(L"CmdMFDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        auto* directionOpt = subcommand->add_option("direction",
                                                    _moveFocusDirection,
                                                    RS_A(L"CmdMoveFocusDirectionArgDesc"));

        directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
        directionOpt->required();
        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            if (_moveFocusDirection != FocusDirection::None)
            {
                MoveFocusArgs args{ _moveFocusDirection };

                ActionAndArgs actionAndArgs{};
                actionAndArgs.Action(ShortcutAction::MoveFocus);
                actionAndArgs.Args(args);

                _startupActions.push_back(std::move(actionAndArgs));
            }
        });
    };

    setupSubcommand(_moveFocusCommand);
    setupSubcommand(_moveFocusShort);
}

// Method Description:
// - Adds the `swap-pane` subcommand and related options to the commandline parser.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildSwapPaneParser()
{
    _swapPaneCommand = _app.add_subcommand("swap-pane", RS_A(L"CmdSwapPaneDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        auto* directionOpt = subcommand->add_option("direction",
                                                    _swapPaneDirection,
                                                    RS_A(L"CmdSwapPaneDirectionArgDesc"));

        directionOpt->transform(CLI::CheckedTransformer(focusDirectionMap, CLI::ignore_case));
        directionOpt->required();
        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            if (_swapPaneDirection != FocusDirection::None)
            {
                SwapPaneArgs args{ _swapPaneDirection };

                ActionAndArgs actionAndArgs{};
                actionAndArgs.Action(ShortcutAction::SwapPane);
                actionAndArgs.Args(args);

                _startupActions.push_back(std::move(actionAndArgs));
            }
        });
    };

    setupSubcommand(_swapPaneCommand);
}

// Method Description:
// - Adds the `focus-pane` subcommand and related options to the commandline parser.
// - Additionally adds the `fp` subcommand, which is just a shortened version of `focus-pane`
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_buildFocusPaneParser()
{
    _focusPaneCommand = _app.add_subcommand("focus-pane", RS_A(L"CmdFocusPaneDesc"));
    _focusPaneShort = _app.add_subcommand("fp", RS_A(L"CmdFPDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        auto* targetOpt = subcommand->add_option("-t,--target",
                                                 _focusPaneTarget,
                                                 RS_A(L"CmdFocusPaneTargetArgDesc"));
        targetOpt->required();
        targetOpt->check(CLI::NonNegativeNumber);
        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            // Build the action from the values we've parsed on the commandline.
            if (_focusPaneTarget >= 0)
            {
                ActionAndArgs focusPaneAction{};
                focusPaneAction.Action(ShortcutAction::FocusPane);
                FocusPaneArgs args{ static_cast<uint32_t>(_focusPaneTarget) };
                focusPaneAction.Args(args);
                _startupActions.push_back(focusPaneAction);
            }
        });
    };

    setupSubcommand(_focusPaneCommand);
    setupSubcommand(_focusPaneShort);
}

void AppCommandlineArgs::_buildSaveSnippetParser()
{
    _saveCommand = _app.add_subcommand("x-save", RS_A(L"SaveSnippetDesc"));

    auto setupSubcommand = [this](auto* subcommand) {
        subcommand->add_option("--name,-n", _saveInputName, RS_A(L"SaveSnippetArgDesc"));
        subcommand->add_option("--keychord,-k", _keyChordOption, RS_A(L"KeyChordArgDesc"));
        subcommand->add_option("command,", _commandline, RS_A(L"CmdCommandArgDesc"));
        subcommand->positionals_at_end(true);

        // When ParseCommand is called, if this subcommand was provided, this
        // callback function will be triggered on the same thread. We can be sure
        // that `this` will still be safe - this function just lets us know this
        // command was parsed.
        subcommand->callback([&, this]() {
            // Build the action from the values we've parsed on the commandline.
            ActionAndArgs saveSnippet{};
            saveSnippet.Action(ShortcutAction::SaveSnippet);
            // First, parse out the commandline in the same way that
            // _getNewTerminalArgs does it
            SaveSnippetArgs args{};

            if (!_commandline.empty())
            {
                std::ostringstream cmdlineBuffer;

                for (const auto& arg : _commandline)
                {
                    if (cmdlineBuffer.tellp() != 0)
                    {
                        // If there's already something in here, prepend a space
                        cmdlineBuffer << ' ';
                    }

                    if (arg.find(" ") != std::string::npos)
                    {
                        cmdlineBuffer << '"' << arg << '"';
                    }
                    else
                    {
                        cmdlineBuffer << arg;
                    }
                }

                args.Commandline(winrt::to_hstring(cmdlineBuffer.str()));
            }

            if (!_keyChordOption.empty())
            {
                args.KeyChord(winrt::to_hstring(_keyChordOption));
            }

            if (!_saveInputName.empty())
            {
                winrt::hstring hString = winrt::to_hstring(_saveInputName);
                args.Name(hString);
            }

            saveSnippet.Args(args);
            _startupActions.push_back(saveSnippet);
        });
    };

    setupSubcommand(_saveCommand);
}

// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
//   that subcommand to support all the properties in a NewTerminalArgs.
// Arguments:
// - subcommand: the command to add the args to.
// Return Value:
// - <none>
void AppCommandlineArgs::_addNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand)
{
    subcommand.profileNameOption = subcommand.subcommand->add_option("-p,--profile",
                                                                     _profileName,
                                                                     RS_A(L"CmdProfileArgDesc"));
    subcommand.sessionIdOption = subcommand.subcommand->add_option("--sessionId",
                                                                   _sessionId,
                                                                   RS_A(L"CmdSessionIdArgDesc"));
    subcommand.startingDirectoryOption = subcommand.subcommand->add_option("-d,--startingDirectory",
                                                                           _startingDirectory,
                                                                           RS_A(L"CmdStartingDirArgDesc"));
    subcommand.titleOption = subcommand.subcommand->add_option("--title",
                                                               _startingTitle,
                                                               RS_A(L"CmdTitleArgDesc"));

    subcommand.tabColorOption = subcommand.subcommand->add_option("--tabColor",
                                                                  _startingTabColor,
                                                                  RS_A(L"CmdTabColorArgDesc"));

    subcommand.suppressApplicationTitleOption = subcommand.subcommand->add_flag(
        "--suppressApplicationTitle,!--useApplicationTitle",
        _suppressApplicationTitle,
        RS_A(L"CmdSuppressApplicationTitleDesc"));

    subcommand.colorSchemeOption = subcommand.subcommand->add_option("--colorScheme",
                                                                     _startingColorScheme,
                                                                     RS_A(L"CmdColorSchemeArgDesc"));

    subcommand.appendCommandLineOption = subcommand.subcommand->add_flag("--appendCommandLine", _appendCommandLineOption, RS_A(L"CmdAppendCommandLineDesc"));

    subcommand.inheritEnvOption = subcommand.subcommand->add_flag(
        "--inheritEnvironment,!--reloadEnvironment",
        _inheritEnvironment,
        RS_A(L"CmdInheritEnvDesc"));

    // Using positionals_at_end allows us to support "wt new-tab -d wsl -d Ubuntu"
    // without CLI11 thinking that we've specified -d twice.
    // There's an alternate construction where we make all subcommands "prefix commands",
    // which lets us get all remaining non-option args provided at the end, but that
    // doesn't support "wt new-tab -- wsl -d Ubuntu -- sleep 10" because the first
    // -- breaks out of the subcommand (instead of the subcommand options).
    // See https://github.com/CLIUtils/CLI11/issues/417 for more info.
    subcommand.commandlineOption = subcommand.subcommand->add_option("command", _commandline, RS_A(L"CmdCommandArgDesc"));
    subcommand.subcommand->positionals_at_end(true);
}

// Method Description:
// - Build a NewTerminalArgs instance from the data we've parsed
// Arguments:
// - <none>
// Return Value:
// - A fully initialized NewTerminalArgs corresponding to values we've currently parsed.
NewTerminalArgs AppCommandlineArgs::_getNewTerminalArgs(AppCommandlineArgs::NewTerminalSubcommand& subcommand)
{
    NewTerminalArgs args{};

    const auto hasCommandline{ !_commandline.empty() };
    if (hasCommandline)
    {
        std::ostringstream cmdlineBuffer;

        for (const auto& arg : _commandline)
        {
            if (cmdlineBuffer.tellp() != 0)
            {
                // If there's already something in here, prepend a space
                cmdlineBuffer << ' ';
            }

            if (arg.find(" ") != std::string::npos)
            {
                cmdlineBuffer << '"' << arg << '"';
            }
            else
            {
                cmdlineBuffer << arg;
            }
        }

        args.Commandline(winrt::to_hstring(cmdlineBuffer.str()));
    }

    if (*subcommand.profileNameOption)
    {
        args.Profile(winrt::to_hstring(_profileName));
    }

    if (*subcommand.sessionIdOption)
    {
        const auto str = winrt::to_hstring(_sessionId);
        const auto id = ::Microsoft::Console::Utils::GuidFromString(str.c_str());
        args.SessionId(id);
    }

    if (*subcommand.startingDirectoryOption)
    {
        args.StartingDirectory(winrt::to_hstring(_startingDirectory));
    }

    if (*subcommand.titleOption)
    {
        args.TabTitle(winrt::to_hstring(_startingTitle));
    }

    if (*subcommand.tabColorOption)
    {
        try
        {
            // This is gonna throw whenever the string that's currently being parsed
            // isn't a valid hex string. Let's just eat anything this throws because
            // we should only lock in the TabColor arg when the user gives a valid hex
            // str, and we shouldn't crash when the user gives us anything else.
            const auto tabColor = Microsoft::Console::Utils::ColorFromHexString(_startingTabColor);
            args.TabColor(static_cast<winrt::Windows::UI::Color>(tabColor));
        }
        catch (...)
        {
        }
    }

    if (*subcommand.suppressApplicationTitleOption)
    {
        args.SuppressApplicationTitle(_suppressApplicationTitle);
    }

    if (*subcommand.colorSchemeOption)
    {
        args.ColorScheme(winrt::to_hstring(_startingColorScheme));
    }
    if (*subcommand.appendCommandLineOption)
    {
        args.AppendCommandLine(_appendCommandLineOption);
    }

    bool inheritEnv = hasCommandline;
    if (*subcommand.inheritEnvOption)
    {
        inheritEnv = _inheritEnvironment;
    }
    args.ReloadEnvironmentVariables(!inheritEnv);

    return args;
}

// Method Description:
// - This function should return true if _no_ subcommands were parsed from the
//   given commandline. In that case, we'll fall back to trying the commandline
//   as a new tab command.
// Arguments:
// - <none>
// Return Value:
// - true if no sub commands were parsed.
bool AppCommandlineArgs::_noCommandsProvided()
{
    return !(*_newTabCommand.subcommand ||
             *_newTabShort.subcommand ||
             *_focusTabCommand ||
             *_focusTabShort ||
             *_moveFocusCommand ||
             *_moveFocusShort ||
             *_movePaneCommand ||
             *_movePaneShort ||
             *_swapPaneCommand ||
             *_focusPaneCommand ||
             *_focusPaneShort ||
             *_newPaneShort.subcommand ||
             *_newPaneCommand.subcommand ||
             *_saveCommand);
}

// Method Description:
// - Reset any state we might have accumulated back to its default values. Since
//   we'll be re-using these members across the parsing of many commandlines, we
//   need to make sure the state from one run doesn't pollute the following one.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::_resetStateToDefault()
{
    _profileName.clear();
    _sessionId.clear();
    _startingDirectory.clear();
    _startingTitle.clear();
    _startingTabColor.clear();
    _commandline.clear();
    _suppressApplicationTitle = false;
    _appendCommandLineOption = false;

    _splitVertical = false;
    _splitHorizontal = false;
    _splitPaneSize = 0.5f;
    _splitDuplicate = false;

    _movePaneTabIndex = -1;
    _focusTabIndex = -1;
    _focusNextTab = false;
    _focusPrevTab = false;

    _moveFocusDirection = FocusDirection::None;
    _swapPaneDirection = FocusDirection::None;

    _focusPaneTarget = -1;
    _loadPersistedLayoutIdx = -1;

    // DON'T clear _launchMode here! This will get called once for every
    // subcommand, so we don't want `wt -F new-tab ; split-pane` clearing out
    // the "global" fullscreen flag (-F).
    // Same with _windowTarget, _position and _size.
}

// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
//   Commandline represents a single command to parse. These commands can be
//   separated by ";", which indicates the start of the next commandline. If the
//   user would like to provide ';' in the text of the commandline, they can
//   escape it as "\;".
// Arguments:
// - args: an array of arguments to parse into Commandlines
// Return Value:
// - a list of Commandline objects, where each one represents a single
//   commandline to parse.
std::vector<Commandline> AppCommandlineArgs::BuildCommands(winrt::array_view<const winrt::hstring> args)
{
    std::vector<Commandline> commands;
    commands.emplace_back(Commandline{});

    // For each arg in argv:
    // Check the string for a delimiter.
    // * If there isn't a delimiter, add the arg to the current commandline.
    // * If there is a delimiter, split the string at that delimiter. Add the
    //   first part of the string to the current command, and start a new
    //   command with the second bit.
    for (const auto& arg : args)
    {
        _addCommandsForArg(commands, { arg });
    }

    return commands;
}

// Function Description:
// - Builds a list of Commandline objects for the given argc,argv. Each
//   Commandline represents a single command to parse. These commands can be
//   separated by ";", which indicates the start of the next commandline. If the
//   user would like to provide ';' in the text of the commandline, they can
//   escape it as "\;".
// Arguments:
// - argc: the number of arguments provided in argv
// - argv: a c-style array of wchar_t strings. These strings can include spaces in them.
// Return Value:
// - a list of Commandline objects, where each one represents a single
//   commandline to parse.
std::vector<Commandline> AppCommandlineArgs::BuildCommands(const std::vector<const wchar_t*>& args)
{
    std::vector<Commandline> commands;
    // Initialize a first Commandline without a leading `wt.exe` argument. When
    // we're run from the commandline, `wt.exe` (or whatever the exe's name is)
    // will be the first argument passed to us
    commands.resize(1);

    // For each arg in argv:
    // Check the string for a delimiter.
    // * If there isn't a delimiter, add the arg to the current commandline.
    // * If there is a delimiter, split the string at that delimiter. Add the
    //   first part of the string to the current command, and start a new
    //   command with the second bit.
    for (const auto& arg : args)
    {
        _addCommandsForArg(commands, { arg });
    }

    return commands;
}

// Function Description:
// - Update and append Commandline objects for the given arg to the given list
//   of commands. Each Commandline represents a single command to parse. These
//   commands can be separated by ";", which indicates the start of the next
//   commandline. If the user would like to provide ';' in the text of the
//   commandline, they can escape it as "\;".
// - As we parse arg, if it doesn't contain a delimiter in it, we'll add it to
//   the last command in commands. Otherwise, we'll generate a new Commandline
//   object for each command in arg.
// Arguments:
// - commands: a list of Commandline objects to modify and append to
// - arg: a single argument that should be parsed into args to append to the
//   current command, or create more Commandlines
// Return Value:
// <none>
void AppCommandlineArgs::_addCommandsForArg(std::vector<Commandline>& commands, std::wstring_view arg)
{
    std::wstring remaining{ arg };
    std::wsmatch match;
    // Keep looking for matches until we've found no unescaped delimiters,
    // or we've hit the end of the string.
    std::regex_search(remaining, match, AppCommandlineArgs::_commandDelimiterRegex);
    do
    {
        if (match.empty())
        {
            // Easy case: no delimiter. Add it to the current command.
            commands.back().AddArg(remaining);
            break;
        }
        else
        {
            // Harder case: There was a match.

            // Regex will include the last character of the string before the delimiter. (see _commandDelimiterRegex)
            // If the match was at the beginning of the string then there is no last character
            // so we can use the length of the match to determine if it was at the beginning.
            const auto matchedFirstChar = match[0].length() == 1;
            // If the match was at the beginning of the string, then the
            // next arg should be "", since there was no content before the
            // delimiter. Otherwise, add one, since the regex will include
            // the last character of the string before the delimiter.
            const auto delimiterPosition = matchedFirstChar ? match.position(0) : match.position(0) + 1;
            const auto nextArg = remaining.substr(0, delimiterPosition);

            if (!nextArg.empty())
            {
                commands.back().AddArg(nextArg);
            }

            // Create a new commandline
            commands.emplace_back(Commandline{});
            // Initialize it with "wt.exe" as the first arg, as if that command
            // was passed individually by the user on the commandline.
            commands.back().AddArg(std::wstring{ AppCommandlineArgs::PlaceholderExeName });

            // Look for the next match in the string, but updating our
            // remaining to be the text after the match.
            remaining = match.suffix().str();
            std::regex_search(remaining, match, AppCommandlineArgs::_commandDelimiterRegex);
        }
    } while (!remaining.empty());
}

// Method Description:
// - Returns the deque of actions we've buffered as a result of parsing commands.
// Arguments:
// - <none>
// Return Value:
// - the deque of actions we've buffered as a result of parsing commands.
std::vector<ActionAndArgs>& AppCommandlineArgs::GetStartupActions()
{
    return _startupActions;
}

// Method Description:
// - Get the string of text that should be displayed to the user on exit. This
//   is usually helpful for cases where the user entered some sort of invalid
//   commandline. It's additionally also used when the user has requested the
//   help text.
// Arguments:
// - <none>
// Return Value:
// - The help text, or an error message, generated from parsing the input
//   provided by the user.
const std::string& AppCommandlineArgs::GetExitMessage() const noexcept
{
    return _exitMessage;
}

// Method Description:
// - Returns true if we should exit the application before even starting the
//   window. We might want to do this if we're displaying an error message or
//   the version string, or if we want to open the settings file.
// Arguments:
// - <none>
// Return Value:
// - true iff we should exit the application before even starting the window
bool AppCommandlineArgs::ShouldExitEarly() const noexcept
{
    return _shouldExitEarly;
}

// Method Description:
// - Ensure that the first command in our list of actions is a NewTab action.
//   This makes sure that if the user passes a commandline like "wt split-pane
//   -H", we _first_ create a new tab, so there's always at least one tab.
// - If the first command in our queue of actions is a NewTab action, this does
//   nothing.
// - This should only be called once - if the first NewTab action is popped from
//   our _startupActions, calling this again will add another.
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::ValidateStartupCommands()
{
    // If we only have a single x-save command, then set our target to the
    // current terminal window. This will prevent us from spawning a new
    // window just to save the commandline.
    if (_startupActions.size() == 1 &&
        _startupActions.front().Action() == ShortcutAction::SaveSnippet &&
        _windowTarget.empty())
    {
        _windowTarget = "0";
    }
    // If we parsed no commands, or the first command we've parsed is not a new
    // tab action, prepend a new-tab command to the front of the list.
    // (also, we don't need to do this if the only action is a x-save)
    else if (_startupActions.empty() ||
             (_startupActions.front().Action() != ShortcutAction::NewTab &&
              _startupActions.front().Action() != ShortcutAction::SaveSnippet))
    {
        // Build the NewTab action from the values we've parsed on the commandline.
        NewTerminalArgs newTerminalArgs{};
        NewTabArgs args{ newTerminalArgs };
        ActionAndArgs newTabAction{ ShortcutAction::NewTab, args };
        // push the arg onto the front
        _startupActions.insert(_startupActions.begin(), 1, newTabAction);
    }
}
std::optional<uint32_t> AppCommandlineArgs::GetPersistedLayoutIdx() const noexcept
{
    return _loadPersistedLayoutIdx >= 0 ?
               std::optional{ static_cast<uint32_t>(_loadPersistedLayoutIdx) } :
               std::nullopt;
}

std::optional<winrt::Microsoft::Terminal::Settings::Model::LaunchMode> AppCommandlineArgs::GetLaunchMode() const noexcept
{
    return _launchMode;
}

std::optional<winrt::Microsoft::Terminal::Settings::Model::LaunchPosition> AppCommandlineArgs::GetPosition() const noexcept
{
    return _position;
}

std::optional<til::size> AppCommandlineArgs::GetSize() const noexcept
{
    return _size;
}

// Method Description:
// - Attempts to parse an array of commandline args into a list of
//   commands to execute, and then parses these commands. As commands are
//   successfully parsed, they will generate ShortcutActions for us to be
//   able to execute. If we fail to parse any commands, we'll return the
//   error code from the failure to parse that command, and stop processing
//   additional commands.
// - The first arg in args should be the program name "wt" (or some variant). It
//   will be ignored during parsing.
// Arguments:
// - args: an array of strings to process as a commandline. These args can contain spaces
// Return Value:
// - 0 if the commandline was successfully parsed
int AppCommandlineArgs::ParseArgs(winrt::array_view<const winrt::hstring> args)
{
    if (args.size() == 2 && args[1] == L"-Embedding")
    {
        return 0;
    }

    auto commands = ::TerminalApp::AppCommandlineArgs::BuildCommands(args);

    for (auto& cmdBlob : commands)
    {
        // On one hand, it seems like we should be able to have one
        // AppCommandlineArgs for parsing all of them, and collect the
        // results one at a time.
        //
        // On the other hand, re-using a CLI::App seems to leave state from
        // previous parsings around, so we could get mysterious behavior
        // where one command affects the values of the next.
        //
        // From https://cliutils.github.io/CLI11/book/chapters/options.html:
        // > If that option is not given, CLI11 will not touch the initial
        // > value. This allows you to set up defaults by simply setting
        // > your value beforehand.
        //
        // So we pretty much need the to either manually reset the state
        // each command, or build new ones.
        const auto result = ParseCommand(cmdBlob);

        // If this succeeded, result will be 0. Otherwise, the caller should
        // exit(result), to exit the program.
        if (result != 0)
        {
            return result;
        }
    }

    // If all the args were successfully parsed, we'll have some commands
    // built in _appArgs, which we'll use when the application starts up.
    return 0;
}

// Method Description:
// - Attempts to parse an array of commandline args into a list of
//   commands to execute, and then parses these commands. As commands are
//   successfully parsed, they will generate ShortcutActions for us to be
//   able to execute. If we fail to parse any commands, we'll return the
//   error code from the failure to parse that command, and stop processing
//   additional commands.
// - The first arg in args should be the program name "wt" (or some variant). It
//   will be ignored during parsing.
// Arguments:
// - args: ExecuteCommandlineArgs describing the command line to parse
// Return Value:
// - 0 if the commandline was successfully parsed
int AppCommandlineArgs::ParseArgs(const winrt::Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args)
{
    if (!args || args.Commandline().empty())
    {
        return 0;
    }

    // Convert the commandline into an array of args with
    // CommandLineToArgvW, similar to how the app typically does when
    // called from the commandline.
    auto argc = 0;
    wil::unique_any<LPWSTR*, decltype(&::LocalFree), ::LocalFree> argv{ CommandLineToArgvW(args.Commandline().c_str(), &argc) };
    if (argv)
    {
        std::vector<winrt::hstring> args;

        // Make sure the first argument is wt.exe, because ParseArgs will
        // always skip the program name. The particular value of this first
        // string doesn't terribly matter.
        args.emplace_back(L"wt.exe");
        for (auto& elem : wil::make_range(argv.get(), argc))
        {
            args.emplace_back(elem);
        }
        winrt::array_view<const winrt::hstring> argsView{ args };
        return ParseArgs(argsView);
    }
    return 0;
}

// Method Description:
// - Allows disabling addition of help-related info in the exit message
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::DisableHelpInExitMessage()
{
    _app.set_help_flag();
    _app.set_help_all_flag();
}

// Method Description:
// - Resets the state to allow external consumers to reuse this instance
// Arguments:
// - <none>
// Return Value:
// - <none>
void AppCommandlineArgs::FullResetState()
{
    _resetStateToDefault();

    _currentCommandline = nullptr;
    _launchMode = std::nullopt;
    _startupActions.clear();
    _exitMessage = "";
    _shouldExitEarly = false;

    _windowTarget = {};
}

std::string_view AppCommandlineArgs::GetTargetWindow() const noexcept
{
    return _windowTarget;
}
